package org.apache.solr.ocean.util;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import org.apache.solr.ocean.replication.ByteBufferPool;
import org.apache.solr.ocean.replication.ByteBufferPool.ByteBuffer;
import org.apache.solr.ocean.replication.ByteBufferPool.ByteBuffers;


public class ByteArrayOutputStream extends OutputStream {
	private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

	private List<ByteBuffer> buffers = new ArrayList<ByteBuffer>();
	/** The index of the current buffer. */
	private int currentBufferIndex;
	/** The total count of bytes in all the filled buffers. */
	private int filledBufferSum;
	/** The current buffer. */
	private ByteBuffer currentBuffer;
	/** The total count of bytes written. */
	private int count;
	private ByteBufferPool byteBufferPool;

	/**
	 * Creates a new byte array output stream. The buffer capacity is initially
	 * 1024 bytes, though its size increases if necessary.
	 */
	public ByteArrayOutputStream(ByteBufferPool byteBufferPool) {
		this.byteBufferPool = byteBufferPool;
	}
  
	public ByteBuffers getByteBuffers() {
		return new ByteBuffers(buffers, size());
	}
	
	/**
	 * Return the appropriate <code>byte[]</code> buffer specified by index.
	 * 
	 * @param index
	 *          the index of the buffer required
	 * @return the buffer
	 */
	private ByteBuffer getBuffer(int index) {
		return (ByteBuffer) buffers.get(index);
	}

	/**
	 * Makes a new buffer available either by allocating a new one or re-cycling
	 * an existing one.
	 * 
	 * @param newcount
	 *          the size of the buffer if one is created
	 */
	private void needNewBuffer(int newcount) {
		if (currentBufferIndex < buffers.size() - 1) {
			// Recycling old buffer
			filledBufferSum += currentBuffer.getBytes().length;

			currentBufferIndex++;
			currentBuffer = getBuffer(currentBufferIndex);
		} else {
			// Creating new buffer
			int newBufferSize = 1024 * 16;
			/**
			 * if (currentBuffer == null) { newBufferSize = newcount; filledBufferSum =
			 * 0; } else { newBufferSize = Math.max(currentBuffer.getBytes().length <<
			 * 1, newcount - filledBufferSum); filledBufferSum +=
			 * currentBuffer.getBytes().length; }
			 */
			currentBufferIndex++;
			currentBuffer = byteBufferPool.get(newBufferSize);
			buffers.add(currentBuffer);
		}
	}

	/**
	 * @see java.io.OutputStream#write(byte[], int, int)
	 */
	public void write(byte[] b, int off, int len) {
		if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) {
			throw new IndexOutOfBoundsException();
		} else if (len == 0) {
			return;
		}
		int newcount = count + len;
		int remaining = len;
		int inBufferPos = count - filledBufferSum;
		while (remaining > 0) {
			int part = Math.min(remaining, currentBuffer.getBytes().length - inBufferPos);
			System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part);
			remaining -= part;
			if (remaining > 0) {
				needNewBuffer(newcount);
				inBufferPos = 0;
			}
		}
		count = newcount;
	}

	/**
	 * @see java.io.OutputStream#write(int)
	 */
	public void write(int b) {
		int inBufferPos = count - filledBufferSum;
		if (inBufferPos == currentBuffer.getBytes().length) {
			needNewBuffer(count + 1);
			inBufferPos = 0;
		}
		currentBuffer.getBytes()[inBufferPos] = (byte) b;
		count++;
	}

	/**
	 * @see java.io.ByteArrayOutputStream#size()
	 */
	public int size() {
		return count;
	}

	/**
	 * Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in
	 * this class can be called after the stream has been closed without
	 * generating an <tt>IOException</tt>.
	 * 
	 * @throws IOException
	 *           never (this method should not declare this exception but it has
	 *           to now due to backwards compatability)
	 */
	public void close() {
		for (ByteBuffer byteBuffer : buffers) {
			byteBuffer.finished();
		}
	}

	/**
	 * @see java.io.ByteArrayOutputStream#reset()
	 */
	public void reset() {
		close();
		count = 0;
		filledBufferSum = 0;
		currentBufferIndex = 0;
		currentBuffer = getBuffer(currentBufferIndex);
	}

	/**
	 * Writes the entire contents of this byte stream to the specified output
	 * stream.
	 * 
	 * @param out
	 *          the output stream to write to
	 * @throws IOException
	 *           if an I/O error occurs, such as if the stream is closed
	 * @see java.io.ByteArrayOutputStream#writeTo(OutputStream)
	 */
	public void writeTo(OutputStream out) throws IOException {
		int remaining = count;
		for (int i = 0; i < buffers.size(); i++) {
			byte[] buf = getBuffer(i).getBytes();
			int c = Math.min(buf.length, remaining);
			out.write(buf, 0, c);
			remaining -= c;
			if (remaining == 0) {
				break;
			}
		}
	}

	/**
	 * Gets the curent contents of this byte stream as a byte array. The result is
	 * independent of this stream.
	 * 
	 * @return the current contents of this output stream, as a byte array
	 * @see java.io.ByteArrayOutputStream#toByteArray()
	 * 
	 * public synchronized byte[] toByteArray() { int remaining = count; if
	 * (remaining == 0) { return EMPTY_BYTE_ARRAY; } byte newbuf[] = new
	 * byte[remaining]; int pos = 0; for (int i = 0; i < buffers.size(); i++) {
	 * byte[] buf = getBuffer(i); int c = Math.min(buf.length, remaining);
	 * System.arraycopy(buf, 0, newbuf, pos, c); pos += c; remaining -= c; if
	 * (remaining == 0) { break; } } return newbuf; }
	 */
}
